# LeetCode 410、分割数组的最大值
# 一、题目描述
给定一个非负整数数组 nums
和一个整数 m
,你需要将这个数组分成 m
个非空的连续子数组。
设计一个算法使得这 m
个子数组各自和的最大值最小。
示例 1:
输入:nums = [7,2,5,10,8], m = 2 输出:18 解释: 一共有四种方法将 nums 分割为 2 个子数组。 其中最好的方式是将其分为 [7,2,5] 和 [10,8] 。 因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
示例 2:
输入:nums = [1,2,3,4,5], m = 2 输出:9
示例 3:
输入:nums = [1,4,4], m = 3 输出:4
提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 106
1 <= m <= min(50, nums.length)
# 二、题目解析
# 三、参考代码
// 登录 AlgoMooc 官网获取更多算法图解
// https://www.algomooc.com
// 作者:程序员吴师兄
// 微信:wzb_3377
// 代码有看不懂的地方一定要私聊咨询吴师兄呀
// 分割数组的最大值(LeetCode 410):https://leetcode.cn/problems/split-array-largest-sum/
class Solution {
public int splitArray(int[] nums, int m) {
// m 表示将数组 nums 划分为 m 个子数组
// 对于数组 nums 来说
// 即可以划分为 1 个完整的子数组,子数组包含 nums 里面所有的元素
// 也可以划分为 n 个子数组,每个子数组只包含 1 个元素
// 也就是说,m 的取值范围为 1 <= m <= n
// 假设 nums = [ 7 , 2 , 5 , 10 , 8 ]
// 1、如果 m = 1 ,那么整个数组作为一部分,只有一种划法,最小的最大值为 32
// 2、如果 m = n,那么每个元素作为一个子数组,只有一种划法,从所有元素选取最大值,最小的最大值为 10
// 因此,最大值的最小值的范围为 [10, 32]
// 这里说明一下最大值和最小值的概念
// 最大值:在一个确定的划分之后,有很多个子数组,从所有子数组和里面挑选出一个最大值来
// 最小值:在 m 等于某个数字时,有很多种划分的方法,每一种划法都能有最大值,比较这些最大值,选出最小的来
// left 记录子数组和最大值下界
int left = 0;
// right记录子数组和最大值上界
int right = 0;
for(int num : nums){
// 子数组和最大值下界为数组中元素的最大值,这个时候每个元素表示 1 个子数组
left = Math.max(left, num);
// 子数组和最大值上界位数组中所有元素和,这个时候只有 1 个子数组
right += num;
}
// 在区间 [ left , right ] 中寻找出一个合理的值
// 这个值是使得这 m 个子数组各自和的最大值最小
while(left <= right){
// 猜测中间值为答案
int mid = left + (right - left) / 2;
// 按照这种方案把数组进行划分
// 得到的子区间数 cnt
int cnt = subSplit( nums , mid );
// 开始分析这种划分是否合理
// 1、划分的子数组个数多了
if(cnt > m){
// 为什么会划分的子数组个数多了
// 因为限定的子数组的元素和值小,导致只能不断地去划分子数组
// 为了可以少划分一些,需要提高子数组的元素和值
left = mid + 1;
// 2、划分的子数组个数少了
}else if(cnt < m){
// 为什么会划分的子数组个数少了
// 因为限定的子数组的元素和值大,导致每个子数组可以装很多元素
// 为了可以多划分一些,需要减小子数组的元素和值
right = mid - 1;
// 3、划分的子数组个数刚刚好
}else if(cnt == m){
// 此时,找到了满足分割数 m 的子数组最大值
// 在此基础上继续在左区间寻找
// 这样,才能使得满足分割数且最大值最小
right = mid - 1;
}
}
// while 终止条件是 left > right
// 最后一次跳出 while 之前,left 会等于 right
// 1、如果此时,在这种情况下进行切分的结果刚好等于分割数 m
// 那么为了获取更小的值,right 会向左移动,从而导致 left > right
// 于是,left 就是那个满足分割数为 m 情况下最大值最小的数
// 2、如果此时,在这种情况下进行切分的结果不等于分割数 m
// left 会向右移动,从而导致 left > right
// 最终 left 也恰好等于上一次分割数为 m 的 mid 值
// 结合视频里面的动画进行理解
return left;
}
// 根据子数组和 maxSum 划分数组,返回子数组个数
private int subSplit(int[] nums, int maxSum){
// 默认为划分为一个子数组,即就是 nums 数组
int cnt = 1;
// 当前子数组的元素和
int tmpArrSum = 0;
// 对每个元素进行考虑它是否有资格划分进行
for(int num : nums){
// 如果发现把当前元素和归纳到目前这个数组后
// 导致子数组元素和大于了 maxSum
if(tmpArrSum + num > maxSum){
// 说明,这个元素需要开始另外分配一个子数组了
// 以该元素为界限开始进行划分
cnt++;
// 当前子数组的元素和归零
tmpArrSum = 0;
}
// 当前子数组的元素和进行累加
tmpArrSum += num;
}
// 返回子数组的个数
// 这些子数组每个子数组元素和的最大值都不会超过 maxSum
return cnt;
}
}
Python
# 登录 AlgoMooc 官网获取更多算法图解
# https://www.algomooc.com
# 作者:程序员吴师兄
# 微信:wzb_3377
# 代码有看不懂的地方一定要私聊咨询吴师兄呀
# 分割数组的最大值(LeetCode 410):https://leetcode.cn/problems/split-array-largest-sum/
class Solution:
def splitArray(self, nums: List[int], k: int) -> int:
# m 表示将数组 nums 划分为 m 个子数组
# 对于数组 nums 来说
# 即可以划分为 1 个完整的子数组,子数组包含 nums 里面所有的元素
# 也可以划分为 n 个子数组,每个子数组只包含 1 个元素
# 也就是说,m 的取值范围为 1 <= m <= n
# 假设 nums = [ 7 , 2 , 5 , 10 , 8 ]
# 1、如果 m = 1 ,那么整个数组作为一部分,只有一种划法,最小的最大值为 32
# 2、如果 m = n,那么每个元素作为一个子数组,只有一种划法,从所有元素选取最大值,最小的最大值为 10
# 因此,最大值的最小值的范围为 [10, 32]
# 这里说明一下最大值和最小值的概念
# 最大值:在一个确定的划分之后,有很多个子数组,从所有子数组和里面挑选出一个最大值来
# 最小值:在 m 等于某个数字时,有很多种划分的方法,每一种划法都能有最大值,比较这些最大值,选出最小的来
# left 记录子数组和最大值下界
left = 0
# right记录子数组和最大值上界
right = 0
for num in nums:
# 子数组和最大值下界为数组中元素的最大值,这个时候每个元素表示 1 个子数组
left = max(left, num)
# 子数组和最大值上界位数组中所有元素和,这个时候只有 1 个子数组
right += num
# 在区间 [ left , right ] 中寻找出一个合理的值
# 这个值是使得这 m 个子数组各自和的最大值最小
while left <= right :
# 猜测中间值为答案
mid = left + (right - left) // 2
# 按照这种方案把数组进行划分
# 得到的子区间数 cnt
cnt = self.subSplit( nums , mid )
# 开始分析这种划分是否合理
# 1、划分的子数组个数多了
if cnt > k :
# 为什么会划分的子数组个数多了
# 因为限定的子数组的元素和值小,导致只能不断地去划分子数组
# 为了可以少划分一些,需要提高子数组的元素和值
left = mid + 1
# 2、划分的子数组个数少了
elif cnt < k :
# 为什么会划分的子数组个数少了
# 因为限定的子数组的元素和值大,导致每个子数组可以装很多元素
# 为了可以多划分一些,需要减小子数组的元素和值
right = mid - 1
# 3、划分的子数组个数刚刚好
elif cnt == k :
# 此时,找到了满足分割数 m 的子数组最大值
# 在此基础上继续在左区间寻找
# 这样,才能使得满足分割数且最大值最小
right = mid - 1
# while 终止条件是 left > right
# 最后一次跳出 while 之前,left 会等于 right
# 1、如果此时,在这种情况下进行切分的结果刚好等于分割数 m
# 那么为了获取更小的值,right 会向左移动,从而导致 left > right
# 于是,left 就是那个满足分割数为 m 情况下最大值最小的数
# 2、如果此时,在这种情况下进行切分的结果不等于分割数 m
# left 会向右移动,从而导致 left > right
# 最终 left 也恰好等于上一次分割数为 m 的 mid 值
# 结合视频里面的动画进行理解
return left
# 根据子数组和 maxSum 划分数组,返回子数组个数
def subSplit(self, nums: List[int], maxSum: int) -> int:
# 默认为划分为一个子数组,即就是 nums 数组
cnt = 1
# 当前子数组的元素和
tmpArrSum = 0
# 对每个元素进行考虑它是否有资格划分进行
for num in nums :
# 如果发现把当前元素和归纳到目前这个数组后
# 导致子数组元素和大于了 maxSum
if tmpArrSum + num > maxSum :
# 说明,这个元素需要开始另外分配一个子数组了
# 以该元素为界限开始进行划分
cnt += 1
# 当前子数组的元素和归零
tmpArrSum = 0
# 当前子数组的元素和进行累加
tmpArrSum += num
# 返回子数组的个数
# 这些子数组每个子数组元素和的最大值都不会超过 maxSum
return cnt